using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER
using UnityEngine.U2D;
#endif
using Sprites = UnityEngine.Sprites;


#if UNITY_EDITOR
using UnityEditor;

namespace LFramework
{
// Custom Editor to order the variables in the Inspector similar to Image component
    [CustomEditor(typeof(SlicedFilledImage)), CanEditMultipleObjects]
    public class SlicedFilledImageEditor : Editor
    {
        private SerializedProperty spriteProp, colorProp;
        private GUIContent spriteLabel;

        private void OnEnable()
        {
            spriteProp = serializedObject.FindProperty("m_Sprite");
            colorProp = serializedObject.FindProperty("m_Color");
            spriteLabel = new GUIContent("Source Image");
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            EditorGUILayout.PropertyField(spriteProp, spriteLabel);
            EditorGUILayout.PropertyField(colorProp);
            DrawPropertiesExcluding(serializedObject, "m_Script", "m_Sprite", "m_Color", "m_OnCullStateChanged");

            serializedObject.ApplyModifiedProperties();
        }
    }
}
#endif

namespace LFramework
{
// Credit: https://bitbucket.org/Unity-Technologies/ui/src/2018.4/UnityEngine.UI/UI/Core/Image.cs
    [RequireComponent(typeof(CanvasRenderer))]
    [AddComponentMenu("UI/Sliced Filled Image", 11)]
    public class SlicedFilledImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement,
        ICanvasRaycastFilter
    {
        private static class SetPropertyUtility
        {
            public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
            {
                if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
                    return false;

                currentValue = newValue;
                return true;
            }

            public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
            {
                if ((currentValue == null && newValue == null) ||
                    (currentValue != null && currentValue.Equals(newValue)))
                    return false;

                currentValue = newValue;
                return true;
            }
        }

        public enum FillDirection
        {
            Right = 0,
            Left = 1,
            Up = 2,
            Down = 3
        }

        private static readonly Vector3[] s_Vertices = new Vector3[4];
        private static readonly Vector2[] s_UVs = new Vector2[4];
        private static readonly Vector2[] s_SlicedVertices = new Vector2[4];
        private static readonly Vector2[] s_SlicedUVs = new Vector2[4];

#pragma warning disable 1692
#pragma warning disable IDE1006 // Suppress 'Naming rule violation' warnings
#pragma warning disable 0649
        [SerializeField] private Sprite m_Sprite;

        public Sprite sprite
        {
            get { return m_Sprite; }
            set
            {
                if (SetPropertyUtility.SetClass(ref m_Sprite, value))
                {
                    SetAllDirty();
                    TrackImage();
                }
            }
        }

        [SerializeField] private FillDirection m_FillDirection;

        public FillDirection fillDirection
        {
            get { return m_FillDirection; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref m_FillDirection, value))
                    SetVerticesDirty();
            }
        }

        [Range(0, 1)] [SerializeField] private float m_FillAmount = 1f;

        public float fillAmount
        {
            get { return m_FillAmount; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref m_FillAmount, Mathf.Clamp01(value)))
                    SetVerticesDirty();
            }
        }

        [SerializeField] private bool m_FillCenter = true;

        public bool fillCenter
        {
            get { return m_FillCenter; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref m_FillCenter, value))
                    SetVerticesDirty();
            }
        }

        [SerializeField] private bool m_PreserveBorder = false;

        public bool preserveBorder
        {
            get { return m_PreserveBorder; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref m_PreserveBorder, value))
                    SetVerticesDirty();
            }
        }

        [SerializeField] private float m_PixelsPerUnitMultiplier = 1f;

        public float pixelsPerUnitMultiplier
        {
            get { return m_PixelsPerUnitMultiplier; }
            set { m_PixelsPerUnitMultiplier = Mathf.Max(0.01f, value); }
        }

        public float pixelsPerUnit
        {
            get
            {
                float spritePixelsPerUnit = 100;
                if (activeSprite)
                    spritePixelsPerUnit = activeSprite.pixelsPerUnit;

                float referencePixelsPerUnit = 100;
                if (canvas)
                    referencePixelsPerUnit = canvas.referencePixelsPerUnit;

                return m_PixelsPerUnitMultiplier * spritePixelsPerUnit / referencePixelsPerUnit;
            }
        }
#pragma warning restore 0649

        [NonSerialized] private Sprite m_OverrideSprite;

        public Sprite overrideSprite
        {
            get { return activeSprite; }
            set
            {
                if (SetPropertyUtility.SetClass(ref m_OverrideSprite, value))
                {
                    SetAllDirty();
                    TrackImage();
                }
            }
        }

        private Sprite activeSprite
        {
            get { return m_OverrideSprite != null ? m_OverrideSprite : m_Sprite; }
        }

        public override Texture mainTexture
        {
            get
            {
                if (activeSprite != null)
                    return activeSprite.texture;

                return material != null && material.mainTexture != null ? material.mainTexture : s_WhiteTexture;
            }
        }

        public bool hasBorder
        {
            get
            {
                if (activeSprite != null)
                {
                    Vector4 v = activeSprite.border;
                    return v.sqrMagnitude > 0f;
                }

                return false;
            }
        }

        public override Material material
        {
            get
            {
                if (m_Material != null)
                    return m_Material;

                if (activeSprite && activeSprite.associatedAlphaSplitTexture != null)
                {
#if UNITY_EDITOR
                    if (Application.isPlaying)
#endif
                        return Image.defaultETC1GraphicMaterial;
                }

                return defaultMaterial;
            }
            set { base.material = value; }
        }

        public float alphaHitTestMinimumThreshold { get; set; }
#pragma warning restore IDE1006
#pragma warning restore 1692

        protected SlicedFilledImage()
        {
            useLegacyMeshGeneration = false;
        }

        protected override void OnEnable()
        {
            base.OnEnable();
            TrackImage();
        }

        protected override void OnDisable()
        {
            base.OnDisable();

            if (m_Tracked)
                UnTrackImage();
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();
            m_PixelsPerUnitMultiplier = Mathf.Max(0.01f, m_PixelsPerUnitMultiplier);
        }
#endif

        protected override void OnPopulateMesh(VertexHelper vh)
        {
            if (activeSprite == null)
            {
                base.OnPopulateMesh(vh);
                return;
            }

            GenerateSlicedFilledSprite(vh);
        }

        /// <summary>
        /// Update the renderer's material.
        /// </summary>
        protected override void UpdateMaterial()
        {
            base.UpdateMaterial();

            // Check if this sprite has an associated alpha texture (generated when splitting RGBA = RGB + A as two textures without alpha)
            if (activeSprite == null)
            {
                canvasRenderer.SetAlphaTexture(null);
                return;
            }

            Texture2D alphaTex = activeSprite.associatedAlphaSplitTexture;
            if (alphaTex != null)
                canvasRenderer.SetAlphaTexture(alphaTex);
        }

        private void GenerateSlicedFilledSprite(VertexHelper vh)
        {
            vh.Clear();

            if (m_FillAmount < 0.001f)
                return;

            Rect rect = GetPixelAdjustedRect();
            Vector4 outer = Sprites.DataUtility.GetOuterUV(activeSprite);
            Vector4 padding = Sprites.DataUtility.GetPadding(activeSprite);

            if (!hasBorder)
            {
                Vector2 size = activeSprite.rect.size;

                int spriteW = Mathf.RoundToInt(size.x);
                int spriteH = Mathf.RoundToInt(size.y);

                // Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
                Vector4 vertices = new Vector4(
                    rect.x + rect.width * (padding.x / spriteW),
                    rect.y + rect.height * (padding.y / spriteH),
                    rect.x + rect.width * ((spriteW - padding.z) / spriteW),
                    rect.y + rect.height * ((spriteH - padding.w) / spriteH));

                GenerateFilledSprite(vh, vertices, outer, m_FillAmount);
                return;
            }

            Vector4 inner = Sprites.DataUtility.GetInnerUV(activeSprite);
            Vector4 border = GetAdjustedBorders(activeSprite.border / pixelsPerUnit, rect);

            padding = padding / pixelsPerUnit;

            s_SlicedVertices[0] = new Vector2(padding.x, padding.y);
            s_SlicedVertices[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);

            s_SlicedVertices[1].x = border.x;
            s_SlicedVertices[1].y = border.y;

            s_SlicedVertices[2].x = rect.width - border.z;
            s_SlicedVertices[2].y = rect.height - border.w;

            if (m_PreserveBorder)
            {
                switch (m_FillDirection)
                {
                    case FillDirection.Right:
                    {
                        s_SlicedVertices[0].x = padding.x;
                        s_SlicedVertices[3].x = rect.width * m_FillAmount - padding.z;

                        float width = s_SlicedVertices[3].x - s_SlicedVertices[0].x;
                        if (width < border.x)
                        {
                            s_SlicedVertices[1].x = s_SlicedVertices[3].x;
                            s_SlicedVertices[2].x = s_SlicedVertices[3].x;
                        }
                        else if (width < border.x + border.z)
                        {
                            s_SlicedVertices[1].x = border.x;
                            s_SlicedVertices[2].x = border.x;
                        }
                        else
                        {
                            s_SlicedVertices[1].x = border.x;
                            s_SlicedVertices[2].x = rect.width * m_FillAmount - border.z;
                        }
                    }
                        break;
                    case FillDirection.Left:
                    {
                        s_SlicedVertices[0].x = rect.width * (1f - m_FillAmount) + padding.x;
                        s_SlicedVertices[3].x = rect.width - padding.z;

                        float width = s_SlicedVertices[3].x - s_SlicedVertices[0].x;
                        if (width < border.x)
                        {
                            s_SlicedVertices[1].x = s_SlicedVertices[0].x;
                            s_SlicedVertices[2].x = s_SlicedVertices[0].x;
                        }
                        else if (width < border.x + border.z)
                        {
                            s_SlicedVertices[1].x = rect.width - border.z;
                            s_SlicedVertices[2].x = rect.width - border.z;
                        }
                        else
                        {
                            s_SlicedVertices[1].x = rect.width * (1f - m_FillAmount) + border.x;
                            s_SlicedVertices[2].x = rect.width - border.z;
                        }
                    }
                        break;
                    case FillDirection.Up:
                    {
                        s_SlicedVertices[0].y = padding.y;
                        s_SlicedVertices[3].y = rect.height * m_FillAmount - padding.w;

                        float height = s_SlicedVertices[3].y - s_SlicedVertices[0].y;
                        if (height < border.y)
                        {
                            s_SlicedVertices[1].y = s_SlicedVertices[3].y;
                            s_SlicedVertices[2].y = s_SlicedVertices[3].y;
                        }
                        else if (height < border.y + border.w)
                        {
                            s_SlicedVertices[1].y = border.y;
                            s_SlicedVertices[2].y = border.y;
                        }
                        else
                        {
                            s_SlicedVertices[1].y = border.y;
                            s_SlicedVertices[2].y = rect.height * m_FillAmount - border.w;
                        }
                    }
                        break;
                    case FillDirection.Down:
                    {
                        s_SlicedVertices[0].y = rect.height * (1f - m_FillAmount) + padding.y;
                        s_SlicedVertices[3].y = rect.height - padding.w;

                        float height = s_SlicedVertices[3].y - s_SlicedVertices[0].y;
                        if (height < border.y)
                        {
                            s_SlicedVertices[1].y = s_SlicedVertices[0].y;
                            s_SlicedVertices[2].y = s_SlicedVertices[0].y;
                        }
                        else if (height < border.y + border.w)
                        {
                            s_SlicedVertices[1].y = rect.height - border.w;
                            s_SlicedVertices[2].y = rect.height - border.w;
                        }
                        else
                        {
                            s_SlicedVertices[1].y = rect.height * (1f - m_FillAmount) + border.y;
                            s_SlicedVertices[2].y = rect.height - border.w;
                        }
                    }
                        break;
                }
            }

            for (int i = 0; i < 4; ++i)
            {
                s_SlicedVertices[i].x += rect.x;
                s_SlicedVertices[i].y += rect.y;
            }

            s_SlicedUVs[0] = new Vector2(outer.x, outer.y);
            s_SlicedUVs[1] = new Vector2(inner.x, inner.y);
            s_SlicedUVs[2] = new Vector2(inner.z, inner.w);
            s_SlicedUVs[3] = new Vector2(outer.z, outer.w);

            float rectStartPos;
            float _1OverTotalSize;
            if (m_FillDirection == FillDirection.Left || m_FillDirection == FillDirection.Right)
            {
                rectStartPos = rect.x + padding.x;

                float totalSize = rect.width - padding.x - padding.z;
                _1OverTotalSize = totalSize > 0f ? 1f / totalSize : 1f;
            }
            else
            {
                rectStartPos = rect.y + padding.y;

                float totalSize = rect.height - padding.y - padding.w;
                _1OverTotalSize = totalSize > 0f ? 1f / totalSize : 1f;
            }

            for (int x = 0; x < 3; x++)
            {
                int x2 = x + 1;

                for (int y = 0; y < 3; y++)
                {
                    if (!m_FillCenter && x == 1 && y == 1)
                        continue;

                    int y2 = y + 1;

                    float sliceStart, sliceEnd;
                    switch (m_FillDirection)
                    {
                        case FillDirection.Right:
                            sliceStart = (s_SlicedVertices[x].x - rectStartPos) * _1OverTotalSize;
                            sliceEnd = (s_SlicedVertices[x2].x - rectStartPos) * _1OverTotalSize;
                            break;
                        case FillDirection.Up:
                            sliceStart = (s_SlicedVertices[y].y - rectStartPos) * _1OverTotalSize;
                            sliceEnd = (s_SlicedVertices[y2].y - rectStartPos) * _1OverTotalSize;
                            break;
                        case FillDirection.Left:
                            sliceStart = 1f - (s_SlicedVertices[x2].x - rectStartPos) * _1OverTotalSize;
                            sliceEnd = 1f - (s_SlicedVertices[x].x - rectStartPos) * _1OverTotalSize;
                            break;
                        case FillDirection.Down:
                            sliceStart = 1f - (s_SlicedVertices[y2].y - rectStartPos) * _1OverTotalSize;
                            sliceEnd = 1f - (s_SlicedVertices[y].y - rectStartPos) * _1OverTotalSize;
                            break;
                        default: // Just there to get rid of the "Use of unassigned local variable" compiler error
                            sliceStart = sliceEnd = 0f;
                            break;
                    }

                    if (sliceStart >= m_FillAmount)
                        continue;

                    Vector4 vertices = new Vector4(s_SlicedVertices[x].x, s_SlicedVertices[y].y, s_SlicedVertices[x2].x,
                        s_SlicedVertices[y2].y);
                    Vector4 uvs = new Vector4(s_SlicedUVs[x].x, s_SlicedUVs[y].y, s_SlicedUVs[x2].x, s_SlicedUVs[y2].y);
                    float fillAmount = (m_FillAmount - sliceStart) / (sliceEnd - sliceStart);

                    GenerateFilledSprite(vh, vertices, uvs, fillAmount);
                }
            }
        }

        private Vector4 GetAdjustedBorders(Vector4 border, Rect adjustedRect)
        {
            Rect originalRect = rectTransform.rect;

            for (int axis = 0; axis <= 1; axis++)
            {
                float borderScaleRatio;

                // The adjusted rect (adjusted for pixel correctness) may be slightly larger than the original rect.
                // Adjust the border to match the adjustedRect to avoid small gaps between borders (case 833201).
                if (originalRect.size[axis] != 0)
                {
                    borderScaleRatio = adjustedRect.size[axis] / originalRect.size[axis];
                    border[axis] *= borderScaleRatio;
                    border[axis + 2] *= borderScaleRatio;
                }

                // If the rect is smaller than the combined borders, then there's not room for the borders at their normal size.
                // In order to avoid artefacts with overlapping borders, we scale the borders down to fit.
                float combinedBorders = border[axis] + border[axis + 2];
                if (adjustedRect.size[axis] < combinedBorders && combinedBorders != 0)
                {
                    borderScaleRatio = adjustedRect.size[axis] / combinedBorders;
                    border[axis] *= borderScaleRatio;
                    border[axis + 2] *= borderScaleRatio;
                }
            }

            return border;
        }

        private void GenerateFilledSprite(VertexHelper vh, Vector4 vertices, Vector4 uvs, float fillAmount)
        {
            if (m_FillAmount < 0.001f)
                return;

            float uvLeft = uvs.x;
            float uvBottom = uvs.y;
            float uvRight = uvs.z;
            float uvTop = uvs.w;

            if (fillAmount < 1f)
            {
                if (m_FillDirection == FillDirection.Left || m_FillDirection == FillDirection.Right)
                {
                    if (m_FillDirection == FillDirection.Left)
                    {
                        vertices.x = vertices.z - (vertices.z - vertices.x) * fillAmount;
                        uvLeft = uvRight - (uvRight - uvLeft) * fillAmount;
                    }
                    else
                    {
                        vertices.z = vertices.x + (vertices.z - vertices.x) * fillAmount;
                        uvRight = uvLeft + (uvRight - uvLeft) * fillAmount;
                    }
                }
                else
                {
                    if (m_FillDirection == FillDirection.Down)
                    {
                        vertices.y = vertices.w - (vertices.w - vertices.y) * fillAmount;
                        uvBottom = uvTop - (uvTop - uvBottom) * fillAmount;
                    }
                    else
                    {
                        vertices.w = vertices.y + (vertices.w - vertices.y) * fillAmount;
                        uvTop = uvBottom + (uvTop - uvBottom) * fillAmount;
                    }
                }
            }

            s_Vertices[0] = new Vector3(vertices.x, vertices.y);
            s_Vertices[1] = new Vector3(vertices.x, vertices.w);
            s_Vertices[2] = new Vector3(vertices.z, vertices.w);
            s_Vertices[3] = new Vector3(vertices.z, vertices.y);

            s_UVs[0] = new Vector2(uvLeft, uvBottom);
            s_UVs[1] = new Vector2(uvLeft, uvTop);
            s_UVs[2] = new Vector2(uvRight, uvTop);
            s_UVs[3] = new Vector2(uvRight, uvBottom);

            int startIndex = vh.currentVertCount;

            for (int i = 0; i < 4; i++)
                vh.AddVert(s_Vertices[i], color, s_UVs[i]);

            vh.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
            vh.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
        }

        int ILayoutElement.layoutPriority
        {
            get { return 0; }
        }

        float ILayoutElement.minWidth
        {
            get { return 0; }
        }

        float ILayoutElement.minHeight
        {
            get { return 0; }
        }

        float ILayoutElement.flexibleWidth
        {
            get { return -1; }
        }

        float ILayoutElement.flexibleHeight
        {
            get { return -1; }
        }

        float ILayoutElement.preferredWidth
        {
            get
            {
                if (activeSprite == null)
                    return 0;

                return Sprites.DataUtility.GetMinSize(activeSprite).x / pixelsPerUnit;
            }
        }

        float ILayoutElement.preferredHeight
        {
            get
            {
                if (activeSprite == null)
                    return 0;

                return Sprites.DataUtility.GetMinSize(activeSprite).y / pixelsPerUnit;
            }
        }

        void ILayoutElement.CalculateLayoutInputHorizontal()
        {
        }

        void ILayoutElement.CalculateLayoutInputVertical()
        {
        }

        bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
        {
            if (alphaHitTestMinimumThreshold <= 0)
                return true;

            if (alphaHitTestMinimumThreshold > 1)
                return false;

            if (activeSprite == null)
                return true;

            Vector2 local;
            if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera,
                    out local))
                return false;

            Rect rect = GetPixelAdjustedRect();

            // Convert to have lower left corner as reference point.
            local.x += rectTransform.pivot.x * rect.width;
            local.y += rectTransform.pivot.y * rect.height;

            Rect spriteRect = activeSprite.rect;
            Vector4 border = activeSprite.border;
            Vector4 adjustedBorder = GetAdjustedBorders(border / pixelsPerUnit, rect);

            for (int i = 0; i < 2; i++)
            {
                if (local[i] <= adjustedBorder[i])
                    continue;

                if (rect.size[i] - local[i] <= adjustedBorder[i + 2])
                {
                    local[i] -= (rect.size[i] - spriteRect.size[i]);
                    continue;
                }

                float lerp = Mathf.InverseLerp(adjustedBorder[i], rect.size[i] - adjustedBorder[i + 2], local[i]);
                local[i] = Mathf.Lerp(border[i], spriteRect.size[i] - border[i + 2], lerp);
            }

            // Normalize local coordinates.
            Rect textureRect = activeSprite.textureRect;
            Vector2 normalized = new Vector2(local.x / textureRect.width, local.y / textureRect.height);

            // Convert to texture space.
            float x = Mathf.Lerp(textureRect.x, textureRect.xMax, normalized.x) / activeSprite.texture.width;
            float y = Mathf.Lerp(textureRect.y, textureRect.yMax, normalized.y) / activeSprite.texture.height;

            switch (m_FillDirection)
            {
                case FillDirection.Right:
                    if (x > m_FillAmount)
                        return false;
                    break;
                case FillDirection.Left:
                    if (1f - x > m_FillAmount)
                        return false;
                    break;
                case FillDirection.Up:
                    if (y > m_FillAmount)
                        return false;
                    break;
                case FillDirection.Down:
                    if (1f - y > m_FillAmount)
                        return false;
                    break;
            }

            try
            {
                return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;
            }
            catch (UnityException e)
            {
                Debug.LogError(
                    "Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " +
                    e.Message + " Also make sure to disable sprite packing for this sprite.", this);
                return true;
            }
        }

        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
        }

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            m_FillAmount = Mathf.Clamp01(m_FillAmount);
        }

        // Whether this is being tracked for Atlas Binding
        private bool m_Tracked = false;

#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER
        private static List<SlicedFilledImage> m_TrackedTexturelessImages = new List<SlicedFilledImage>();
        private static bool s_Initialized;
#endif

        private void TrackImage()
        {
            if (activeSprite != null && activeSprite.texture == null)
            {
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER
                if (!s_Initialized)
                {
                    SpriteAtlasManager.atlasRegistered += RebuildImage;
                    s_Initialized = true;
                }

                m_TrackedTexturelessImages.Add(this);
#endif
                m_Tracked = true;
            }
        }

        private void UnTrackImage()
        {
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER
            m_TrackedTexturelessImages.Remove(this);
#endif
            m_Tracked = false;
        }

#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER
        private static void RebuildImage(SpriteAtlas spriteAtlas)
        {
            for (int i = m_TrackedTexturelessImages.Count - 1; i >= 0; i--)
            {
                SlicedFilledImage image = m_TrackedTexturelessImages[i];
                if (spriteAtlas.CanBindTo(image.activeSprite))
                {
                    image.SetAllDirty();
                    m_TrackedTexturelessImages.RemoveAt(i);
                }
            }
        }
#endif
    }
}